#include <iterator>
#include <thread>
#include <fstream>
#include <algorithm>
#include <getopt.h>
#include <boost/filesystem.hpp>

#include "LogWriter.h"
#include "BackupFilesReceiver.h"
#include "BackupFilesRestorer.h"
#include "BackupFilesUploader.h"
#include "UdpIpListParser.h"

#define HELP_FILE_NAME "README"


namespace fs = boost::filesystem;


struct ExecArgs
{
	unsigned short receivePort;
	unsigned short restorePort;
	std::string serversListFilePath;
	std::string uploadDirPath;
	std::string storageDirPath;

	ExecArgs() :
		receivePort(0), restorePort(0)
	{}
};


int parse_exec_args(int argc, char** argv, ExecArgs& execArgs);
void display_help(std::ostream& output);


static fs::path s_helpFilePath;


int main(int argc, char** argv)
{
	(s_helpFilePath = fs::system_complete(fs::path(argv[0])))
		.remove_filename().remove_filename()
		/= HELP_FILE_NAME;

	LogWriter logWriter(std::cerr);
	
	try
	{
		LogWriter receiverLogWriter(std::cerr, "Receiver");
		LogWriter restorerLogWriter(std::cerr, "Restorer");
		LogWriter uploaderLogWriter(std::cerr, "Uploader");
		int parsingRes = 0;
		ExecArgs execArgs;

		if ((parsingRes = parse_exec_args(argc, argv, execArgs)) != 0)
			return parsingRes;

		UdpIpList serverIps;
		BackupFilesReceiver backupFilesReceiver(receiverLogWriter, execArgs.receivePort);
		BackupFilesRestorer backupFilesRestorer(restorerLogWriter, execArgs.restorePort);
		BackupFilesUploader backupFilesUploader(uploaderLogWriter);
		UdpIpListParser serverIpsParser;
		
		serverIpsParser.parse_file(execArgs.serversListFilePath,
			std::inserter(serverIps, serverIps.end()));
		
		backupFilesReceiver.set_storage_directory(execArgs.storageDirPath);
		
		backupFilesRestorer.set_storage_directory(execArgs.storageDirPath);
		
		backupFilesUploader.set_monitored_directory(execArgs.uploadDirPath);
		backupFilesUploader.set_destination_servers(serverIps);
		
		std::thread filesReceiverThread(
			std::bind(&BackupFilesReceiver::run, &backupFilesReceiver));
		std::thread filesRestorerThread(
			std::bind(&BackupFilesRestorer::run, &backupFilesRestorer));
		
		backupFilesUploader.run();
		
		filesRestorerThread.join();
		filesReceiverThread.join();
	}
	catch (std::exception& exc)
	{
		logWriter.write_error(exc.what());
		logWriter.write_info("Use --help for help.");
	}

	return 0;
}


int parse_exec_args(int argc, char** argv, ExecArgs& execArgs)
{
	enum
	{
		HELP_OPT = 300,
		RECEIVE_PORT_OPT,
		RESTORE_PORT_OPT,
		SERVERS_FILE_OPT,
		UPLOAD_DIR_OPT,
		STORAGE_DIR_OPT
	};

	static const struct option S_OPTION_DEFS[] =
		{
			{"help", no_argument, NULL, HELP_OPT},
			{"receivePort", required_argument, NULL, RECEIVE_PORT_OPT},
			{"restorePort", required_argument, NULL, RESTORE_PORT_OPT},
			{"serversFile", required_argument, NULL, SERVERS_FILE_OPT},
			{"uploadDir", required_argument, NULL, UPLOAD_DIR_OPT},
			{"storageDir", required_argument, NULL, STORAGE_DIR_OPT},
			{NULL, 0, NULL, 0}
		};

	bool allArgumentsParsed = false;

	while (!allArgumentsParsed)
	{
		switch (getopt_long(argc, argv, "p:", S_OPTION_DEFS, NULL))
		{
			case HELP_OPT:
				display_help(std::cout);
				return 1;

			case RECEIVE_PORT_OPT:
			{
				if (optarg)
					execArgs.receivePort = atoi(optarg);
			} break;

			case RESTORE_PORT_OPT:
			{
				if (optarg)
					execArgs.restorePort = atoi(optarg);
			} break;

			case SERVERS_FILE_OPT:
			{
				if (!optarg || strcmp(optarg, "") == 0)
					break;

				execArgs.serversListFilePath = optarg;
			} break;

			case UPLOAD_DIR_OPT:
			{
				if (!optarg || strcmp(optarg, "") == 0)
					break;

				execArgs.uploadDirPath = optarg;

				if (!fs::exists(execArgs.uploadDirPath))
					fs::create_directory(fs::path(execArgs.uploadDirPath));
				else if (!fs::is_directory(execArgs.uploadDirPath))
				{
					std::stringstream ss;
					ss << "Container for files to upload \"" << execArgs.uploadDirPath
						<< "\" is not a directory.";
					throw std::invalid_argument(ss.str());
				}
			} break;

			case STORAGE_DIR_OPT:
			{
				if (!optarg || strcmp(optarg, "") == 0)
					break;
				
				execArgs.storageDirPath = optarg;
			
				if (!fs::exists(execArgs.storageDirPath))
					fs::create_directory(fs::path(execArgs.storageDirPath));
				else if (!fs::is_directory(execArgs.storageDirPath))
				{
					std::stringstream ss;
					ss << "Container for file fragments to store \""
						<< execArgs.storageDirPath << "\" is not a directory.";
					throw std::invalid_argument(ss.str());
				}
			} break;

			case -1:
				allArgumentsParsed = true;
				break;

			default:;
		}
	}

	if (execArgs.serversListFilePath.empty())
		throw std::invalid_argument("No servers list file specified.");
	if (execArgs.uploadDirPath.empty())
		throw std::invalid_argument("No directory specified for files to upload.");
	if (execArgs.storageDirPath.empty())
		throw std::invalid_argument("No directory specified for file segments to store.");

	return 0;
}


void display_help(std::ostream& output)
{
	std::ifstream helpFile(s_helpFilePath.native(), std::ios_base::in);

	if (!helpFile.good())
	{
		output << "Sorry, no help available...\n";
		return;
	}

	std::copy(std::istreambuf_iterator<char>(helpFile), std::istreambuf_iterator<char>(),
		std::ostreambuf_iterator<char>(output));
}
